package org.geometerplus.fbreader.book;

import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.TreeMap;

import logic.dao.BookDao;
import logic.dao.BookMarksDao;
import logic.dao.HighlightingStyleDao;

import org.geometerplus.fbreader.bookmodel.BookReadingException;
import org.geometerplus.fbreader.formats.FormatPlugin;
import org.geometerplus.fbreader.formats.PluginCollection;
import org.geometerplus.zlibrary.core.filesystem.ZLFile;
import org.geometerplus.zlibrary.core.filesystem.ZLPhysicalFile;
import org.geometerplus.zlibrary.text.view.ZLTextPosition;

import android.content.Context;

public class BookCollection extends AbstractBookCollection {
	public final List<String> BookDirectories;
	
	private Context myContext;
	private BookDao bookDao;
	private BookMarksDao bookMarkDao;
	private HighlightingStyleDao hhStyleDao;

	private final Map<ZLFile,Book> myBooksByFile =
		Collections.synchronizedMap(new LinkedHashMap<ZLFile,Book>());
	private final Map<Long,Book> myBooksById =
		Collections.synchronizedMap(new HashMap<Long,Book>());
	private final List<String> myFilesToRescan =
		Collections.synchronizedList(new LinkedList<String>());

	private volatile Status myStatus = Status.NotStarted;

	private final Map<Integer,HighlightingStyle> myStyles =
		Collections.synchronizedMap(new TreeMap<Integer,HighlightingStyle>());

	public BookCollection(Context context, List<String> bookDirectories) {
		myContext = context;
		BookDirectories = Collections.unmodifiableList(new ArrayList<String>(bookDirectories));
		bookDao = BookDao.getInstance(myContext);
		bookMarkDao = BookMarksDao.getInstance(myContext);
		hhStyleDao = HighlightingStyleDao.getInstance(myContext);
	}

	public int size() {
		return myBooksByFile.size();
	}

	public Book getBookByFile(ZLFile bookFile) {
		if (bookFile == null) {
			return null;
		}
		final FormatPlugin plugin = PluginCollection.Instance().getPlugin(bookFile);
		if (plugin == null) {
			return null;
		}
		try {
			bookFile = plugin.realBookFile(bookFile);
		} catch (BookReadingException e) {
			return null;
		}

		Book book = myBooksByFile.get(bookFile);
		if (book != null) {
			return book;
		}

		final ZLPhysicalFile physicalFile = bookFile.getPhysicalFile();
		if (physicalFile != null && !physicalFile.exists()) {
			return null;
		}
		
		book = bookDao.loadBookByFile( bookFile);
		
		if (book != null ) {
			//saveBook(book, false);
			// saved
			addBook(book, false);
			return book;
		}

		try {
			if (book == null) {
				book = new Book(bookFile);
			} else {
				book.readMetaInfo();
			}
		} catch (BookReadingException e) {
			return null;
		}
		saveBook(book, false);
		return book;
	}

	public Book getBookById(long id) {
		Book book = myBooksById.get(id);
		if (book != null) {
			return book;
		}

		book = bookDao.loadBook(id);
		if (book == null) {
			return null;
		}

		final ZLFile bookFile = book.File;
		final ZLPhysicalFile physicalFile = bookFile.getPhysicalFile();
		if (physicalFile == null) {
			// loaded from db
			addBook(book, false);
			return book;
		}
		if (!physicalFile.exists()) {
			return null;
		}
		
		try {
			book.readMetaInfo();
			// loaded from db
			addBook(book, false);
			return book;
		} catch (BookReadingException e) {
			return null;
		}
	}


	private void addBook(Book book, boolean force) {
		if (book == null || book.getId() == -1) {
			return;
		}
		synchronized (myBooksByFile) {
			final Book existing = myBooksByFile.get(book.File);
			if (existing == null) {
				myBooksByFile.put(book.File, book);
				myBooksById.put(book.getId(), book);
				fireBookEvent(BookEvent.Added, book);
			} else if (force) {
				existing.updateFrom(book);
				fireBookEvent(BookEvent.Updated, existing);
			}
		}
	}

	public synchronized boolean saveBook(Book book, boolean force) {
		if (book == null) {
			return false;
		}

		final boolean result = book.save(bookDao, force);
		addBook(book, true);
		return result;
	}

	public void removeBook(Book book, boolean deleteFromDisk) {
		synchronized (myBooksByFile) {
			myBooksByFile.remove(book.File);
			myBooksById.remove(book.getId());
			if (deleteFromDisk) {
				book.File.getPhysicalFile().delete();
			}
		}
		fireBookEvent(BookEvent.Removed, book);
	}

	public Status status() {
		return myStatus;
	}

	public List<Book> books(BookQuery query) {
		final List<Book> allBooks;
		synchronized (myBooksByFile) {
			//allBooks = new ArrayList<Book>(new LinkedHashSet<Book>(myBooksByFile.values()));
			allBooks = new ArrayList<Book>(myBooksByFile.values());
		}
		final int start = query.Page * query.Limit;
		if (start >= allBooks.size()) {
			return Collections.emptyList();
		}
		final int end = start + query.Limit;
		if (query.Filter instanceof Filter.Empty) {
			return allBooks.subList(start, Math.min(end, allBooks.size()));
		} else {
			int count = 0;
			final List<Book> filtered = new ArrayList<Book>(query.Limit);
			for (Book b : allBooks) {
				if (query.Filter.matches(b)) {
					if (count >= start) {
						filtered.add(b);
					}
					if (++count == end) {
						break;
					}
				}
			}
			return filtered;
		}
	}

	public boolean hasBooks(Filter filter) {
		final List<Book> allBooks;
		synchronized (myBooksByFile) {
			allBooks = new ArrayList<Book>(myBooksByFile.values());
		}
		for (Book b : allBooks) {
			if (filter.matches(b)) {
				return true;
			}
		}
		return false;
	}


	private void setStatus(Status status) {
		myStatus = status;
		fireBuildEvent(status);
	}

	public synchronized void startBuild() {
		if (myStatus != Status.NotStarted) {
			return;
		}
		setStatus(Status.Started);
		try {
			build();
			setStatus(Status.Succeeded);
		} catch (Throwable t) {
			setStatus(Status.Failed);
		} finally {
			synchronized (myFilesToRescan) {
				processFilesQueue();
			}
		};

//		final Thread builder = new Thread("Library.build") {
//			public void run() {
//				try {
//					build();
//					setStatus(Status.Succeeded);
//				} catch (Throwable t) {
//					setStatus(Status.Failed);
//				} finally {
//					synchronized (myFilesToRescan) {
//						processFilesQueue();
//					}
//				}
//			}
//		};
//		builder.setPriority(Thread.MIN_PRIORITY);
//		builder.start();
	}

	public void rescan(String path) {
		synchronized (myFilesToRescan) {
			myFilesToRescan.add(path);
			processFilesQueue();
		}
	}

	private void processFilesQueue() {
		synchronized (myFilesToRescan) {
			if (!myStatus.IsCompleted) {
				return;
			}

			final Set<ZLFile> filesToRemove = new HashSet<ZLFile>();
			for (String path : myFilesToRescan) {
				path = new ZLPhysicalFile(new File(path)).getPath();
				synchronized (myBooksByFile) {
					for (ZLFile f : myBooksByFile.keySet()) {
						if (f.getPath().startsWith(path)) {
							filesToRemove.add(f);
						}
					}
				}
			}

			for (ZLFile file : collectPhysicalFiles(myFilesToRescan)) {
				// TODO:
				// collect books from archives
				// rescan files and check book id
				filesToRemove.remove(file);
				final Book book = getBookByFile(file);
				if (book != null) {
					saveBook(book, false);
				}
			}

			for (ZLFile f : filesToRemove) {
				synchronized (myBooksByFile) {
					final Book book = myBooksByFile.remove(f);
					if (book != null) {
						myBooksById.remove(book.getId());
						fireBookEvent(BookEvent.Removed, book);
					}
				}
			}
			myFilesToRescan.clear();
		}
	}

	private void build() {
		// Step 0: get database books marked as "existing"
		final Map<Long,Book> savedBooksByFileId = new HashMap<Long,Book>() ;
		final Map<Long,Book> savedBooksByBookId = bookDao.loadBooks();
		for (Book b : savedBooksByBookId.values()) {
			savedBooksByFileId.put(b.getId(), b);
		}

		for (Book book : savedBooksByBookId.values()) {
			final ZLPhysicalFile file = book.File.getPhysicalFile();
			if (file != book.File && file != null && file.getPath().endsWith(".epub")) {
				continue;
			}
			if (book.File.exists()) {
				boolean doAdd = true;
				if (file == null) {
					continue;
				}
				if (doAdd) {
					// loaded from db
					addBook(book, false);
				}
			}
		}
	}

	private List<ZLPhysicalFile> collectPhysicalFiles(List<String> paths) {
		final Queue<ZLPhysicalFile> fileQueue = new LinkedList<ZLPhysicalFile>();
		final HashSet<ZLPhysicalFile> dirSet = new HashSet<ZLPhysicalFile>();
		final LinkedList<ZLPhysicalFile> fileList = new LinkedList<ZLPhysicalFile>();

		for (String p : paths) {
			fileQueue.offer(new ZLPhysicalFile(new File(p)));
		}

		while (!fileQueue.isEmpty()) {
			final ZLPhysicalFile entry = fileQueue.poll();
			if (!entry.exists()) {
				continue;
			}
			if (entry.isDirectory()) {
				if (dirSet.contains(entry)) {
					continue;
				}
				dirSet.add(entry);
				for (ZLFile file : entry.children()) {
					fileQueue.add((ZLPhysicalFile)file);
				}
			} else {
				entry.setCached(true);
				fileList.add(entry);
			}
		}
		return fileList;
	}

	
	public List<Bookmark> bookmarks(BookmarkQuery query) {
		return bookMarkDao.loadBookmarks(query);
	}

	public void saveBookmark(Bookmark bookmark) {
		if (bookmark != null) {
			bookmark.setId(bookMarkDao.saveBookmark(bookmark));
			
			if (bookmark.IsVisible) {
				final Book book = getBookById(bookmark.getBookId());
				if (book != null) {
					book.HasBookmark = true;
					fireBookEvent(BookEvent.BookmarksUpdated, book);
				}
			}
		}
	}

	public void deleteBookmark(Bookmark bookmark) {
		if (bookmark != null && bookmark.getId() != -1) {
			bookMarkDao.deleteBookmark(bookmark);
			if (bookmark.IsVisible) {
				final Book book = getBookById(bookmark.getBookId());
				if (book != null) {
					book.HasBookmark = bookMarkDao.hasVisibleBookmark(bookmark.getBookId());
					fireBookEvent(BookEvent.BookmarksUpdated, book);
				}
			}
		}
	}

	public ZLTextPosition getStoredPosition(long bookId, String chapterId) {
		return bookMarkDao.getStoredPosition(bookId, chapterId);
	}

	public void storePosition(long bookId, String chapterId, ZLTextPosition position) {
		if (bookId != -1) {
			bookMarkDao.storePosition(bookId, chapterId, position);
		}
	}

	private synchronized void initStylesTable() {
		if (myStyles.isEmpty()) {
			for (HighlightingStyle style : hhStyleDao.loadStyles()) {
				myStyles.put(style.Id, style);
			}
		}
	}

	public HighlightingStyle getHighlightingStyle(int styleId) {
		initStylesTable();
		return myStyles.get(styleId);
	}

	public List<HighlightingStyle> highlightingStyles() {
		initStylesTable();
		return new ArrayList<HighlightingStyle>(myStyles.values());
	}

	public void saveHighlightingStyle(HighlightingStyle style) {
		myStyles.put(style.Id, style);
		hhStyleDao.saveStyle(style);
		fireBookEvent(BookEvent.BookmarkStyleChanged, null);
	}
}
